/**
 * @file socket.c
 *
 * @brief socket management
 *
 * A set of wrapping function around POSIX socket functions.
 *
 * @author Richard A Delorme
 * @date 1998 - 2011
 * @version 4.1
 */

#if defined(__unix__) || defined(__APPLE__)

#include "util.h"
#include "socket.h"

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <time.h>
#include <sys/resource.h>
#include <sys/types.h>
#include <arpa/inet.h>
#include <netdb.h>

Log socket_log[1];

/**
 * @brief Connect to host + port 
 *
 * @param host an internet address or an IP.
 * @param port telnet port (5000).
 * @return a socket descriptor (or -1 in case of failure).
 */
int socket_connect(const char *host, int port) {
	int sock, ip, err;
	struct sockaddr_in sa;

	memset(&sa, 0, sizeof(sa));
	errno = h_errno = 0;
	ip = inet_addr(host);
	if (ip == -1) {
		struct hostent *hp = gethostbyname(host);
		if (hp == NULL) {
			error("\thost = %s\n\tport= %d\n\tIP = %d\n\th_errno = %d: %s\n", host, port, ip, h_errno, hstrerror(h_errno));
			return -1;
		}
		memcpy((char*)&sa.sin_addr, hp->h_addr_list[0], hp->h_length);
	} else {
		memcpy((char*)&sa.sin_addr, &ip, sizeof ip);
	}
	sa.sin_family = AF_INET;
	sa.sin_port = htons((unsigned short)port);

	errno = h_errno = 0;
	sock = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (sock < 0) {
		error("\tsocket(domain = %d\n\ttype = %d\n\tprotocol = %d)\n", AF_INET, SOCK_STREAM, IPPROTO_TCP);
		return -1;
	}

	err = connect(sock, (struct sockaddr*) &sa, sizeof sa);
	if (err < 0) {
		error("\tconnect(sock = %d, (host = %s, port= %d, IP = %d)) = %d\n", sock, host, port, ip, err);
		close(sock);
		return -1;
	}

	log_open(socket_log, options.socket_log_file);

	return sock;
}

/**
 * @brief Disconnect.
 *
 * Shutdown a link and close the socket.
 *
 * @param sock socket descriptor.
 * @param how tell if reception or transmission should be disconnected.
 * @return a socket descriptor (or -1 in case of failure).
 */
void socket_shutdown(int sock, int how){
	int err;

	log_close(socket_log);

	errno = 0;
	err = shutdown(sock, how);
	if(err) {
		fatal_error("\tshutdown(%d, %d) = %d\n", sock, how, err);
	}

	errno = 0;
	err = close(sock);
	if (err) {
		fatal_error("\terror while closing socket %d\n", sock);
	}
}

/**
 * @brief Read from a socket to a buffer. 
 *
 * @param sock Socket descriptor.
 * @param buffer Pointer to a buffer.
 * @param flag Reception options.
 */
int socket_read(int sock, char **buffer, int flag){
	int buffer_length, recv_length;
	int first;
	const int BUCKET_SIZE = 1024;

	if (sock < 0) return 0;

	if (*buffer == NULL) buffer_length = 0;
	else buffer_length = strlen(*buffer);
	for (first = 1;; first = 0){
		errno = 0;
		*buffer = realloc(*buffer, buffer_length + BUCKET_SIZE + 1);
		if (*buffer == NULL) fatal_error("Cannot allocate buffer with %d chars\n", buffer_length + BUCKET_SIZE + 1) ;
		recv_length = recv(sock, *buffer + buffer_length, BUCKET_SIZE, flag);

		if (recv_length == 0 && first) {
			errno = 0;
			return 0; // when quitting?
		}
		if (recv_length <= 0) {
			if(errno == EAGAIN || errno == EINTR || errno == 0) return 1;
			error("recv(%d, %p, %d, %d) = %d\n", sock, *buffer, BUCKET_SIZE, flag, recv_length);
			socket_option_dump(sock, stderr);
			return 0;
		}
		fwrite(*buffer + buffer_length, recv_length, sizeof(char), stdout);
		buffer_length += recv_length;
		(*buffer)[buffer_length] = '\0';

		log_print(socket_log, "recv<%s\n", *buffer + buffer_length);
	}
}

/**
 * @brief Write to a socket from a buffer 
 *
 * @param sock Socket descriptor.
 * @param buffer Pointer to a buffer.
 * @param flag Transmission options.
 */
int socket_write(int sock, char *buffer, int flag){
	int buffer_length, send_length;
	int first;
	char *i, *o;
	const int BUCKET_SIZE = 1024;

	if (sock < 0) return 0;

	buffer_length = strlen(buffer);
	for (first = 1; buffer_length; first = 0) {
		errno = 0;
		send_length = send(sock, buffer, buffer_length, flag);

		if (send_length == 0 && first) return 0;
		if (send_length <= 0) {
			if (errno == EAGAIN || errno == EINTR || errno == 0) return 1;
			error("send(%d, %p, %d, %d) = %d\n", sock, buffer, BUCKET_SIZE, flag, send_length);
			socket_option_dump(sock, stderr);
			return 0;
		}
		log_print(socket_log, "send<%.*s\n", send_length, buffer);
		for (o = buffer, i = buffer + send_length; *i;) *o++ = *i++;
		buffer_length -= send_length;
		buffer[buffer_length] = '\0';
	}
	return 1;
}

/**
 * @brief get a socket option 
 */
int socket_option_get(int sock, int level, int option_name, void *option_value, socklen_t *option_length){
	int err;

	errno = 0;
	err = getsockopt(sock, level, option_name, option_value, option_length);
	if (err) {
		fatal_error("getsockopt(%d, %d, %d, %p, %p) = %d\n", sock, level, option_name, option_value, (void *)option_length, err);
	}

	return err;
}

/**
 * @brief Set a socket option 
 */
int socket_option_set(int sock, int level, int option_name, void *option_value, socklen_t option_length){
	int err;

	errno = 0;
	err = setsockopt(sock, level, option_name, option_value, option_length);
	if (err)	{
		fatal_error("setsockopt(%d, %d, %d, %p, %d) = %d\n", sock, level, option_name, option_value, option_length, err);
	}
	return err;	
}

/**
 * @brief print a socket option. 
 */
void socket_option_print(int sock, int option_name, const char *option_string, FILE *f){
	int option_value;
	socklen_t option_length;
	
	socket_option_get(sock, SOL_SOCKET, option_name, &option_value, &option_length);
	fprintf(f, "%.20s(%d) = %d\n", option_string, sock, option_value);
}

#define SOCKET_OPTION_PRINT(sock, name, f) socket_option_print(sock, name, #name, f)

/**
 * @brief Dump all socket options.
 */
void socket_option_dump(int sock, FILE *f) {
	fprintf(f, "socket_options:\n");
	SOCKET_OPTION_PRINT(sock, SO_DEBUG, f);
	SOCKET_OPTION_PRINT(sock, SO_REUSEADDR, f);
	SOCKET_OPTION_PRINT(sock, SO_TYPE, f);
	SOCKET_OPTION_PRINT(sock, SO_ERROR, f);
	SOCKET_OPTION_PRINT(sock, SO_DONTROUTE, f);
	SOCKET_OPTION_PRINT(sock, SO_BROADCAST, f);
	SOCKET_OPTION_PRINT(sock, SO_SNDBUF, f);
	SOCKET_OPTION_PRINT(sock, SO_RCVBUF, f);
	SOCKET_OPTION_PRINT(sock, SO_KEEPALIVE, f);
	SOCKET_OPTION_PRINT(sock, SO_OOBINLINE, f);
	SOCKET_OPTION_PRINT(sock, SO_LINGER, f);
	SOCKET_OPTION_PRINT(sock, SO_SNDLOWAT, f);
	SOCKET_OPTION_PRINT(sock, SO_RCVLOWAT, f);
	SOCKET_OPTION_PRINT(sock, SO_SNDTIMEO, f);
	SOCKET_OPTION_PRINT(sock, SO_RCVTIMEO, f);
}
#undef SOCKET_OPTION_PRINT

#endif
